package net.alpenviewer.map;

import java.awt.image.BufferedImage;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Arrays;

import org.geotools.measure.Latitude;
import org.geotools.measure.Longitude;
import org.geotools.measure.Measure;

public final class MapBuilder {

	public static BufferedImage createElevationImage(
			SRTM srtm, 
			ColorEncoder cm) throws IOException {

		final int res = 1 + 3600 / srtm.arcSecond;
		final BufferedImage img = new BufferedImage(res, res, BufferedImage.TYPE_INT_ARGB);
		final File src = srtm.src;		

		DataInputStream is = null;
		try {
			final byte[] buf = new byte[res*2];
			is = new DataInputStream(new FileInputStream(src));

			for (int y=0; y<res; y++) { // row (y-axis)
				is.read(buf);

				for (int x=0; x<res; x++) { // column (x-axis)
					final short height = (short) ( ((buf[x*2] & 0xff) << 0x8) | (buf[x*2+1] & 0xff) );
					img.setRGB(x, y, cm.getEncodedColor(height));															
				}
			}

		} catch (IOException e) {
			throw new RuntimeException(e);
		}finally {
			try { is.close(); } catch (Exception ignored) {}
		}				

		return img;
	}

	public static BufferedImage createGradientImage(
			SRTM srtm,
			ColorEncoder cm) {

		final int res = 1 + 3600 / srtm.arcSecond;		
		final File src = srtm.src;		
		final SRTM[] neighbors = SRTM.getNeighborSrtm(srtm);

		final short data[] = new short[(res+2)*(res+2)]; 
		Arrays.fill(data, Short.MIN_VALUE);

		DataInputStream srtmis = null;
		DataInputStream[] neighborIs = new DataInputStream[neighbors.length];
		try {
			srtmis = new DataInputStream(new FileInputStream(src));
			final byte[] buf = new byte[res*2];

			// center
			for (int y=0; y<res; y++) { // row (y-axis)
				srtmis.read(buf);				
				for (int x=0; x<res; x++) { // column (x-axis)
					final short height = (short) ( ((buf[x*2] & 0xff) << 0x8) | (buf[x*2+1] & 0xff) );
					data[(res+2)*(y+1) + x + 1] = height;
				}
			}
			srtmis.close();


			// nw
			if (neighbors[0] != null) {
				neighborIs[0] = new DataInputStream(new FileInputStream(neighbors[0].src));
				neighborIs[0].skipBytes( 2 * res * (res - 1));
				neighborIs[0].read(buf);

				final short height = (short) ( ((buf[res*2-2] & 0xff) << 0x8) | (buf[res*2-1] & 0xff) );				
				data[0] = height;		
				neighborIs[0].close();
			}		

			// ne
			if (neighbors[2] != null) {
				neighborIs[2] = new DataInputStream(new FileInputStream(neighbors[2].src));
				neighborIs[2].skipBytes( 2 * res * (res - 1));
				neighborIs[2].read(buf);

				final short height = (short) ( ((buf[0] & 0xff) << 0x8) | (buf[1] & 0xff) );				
				data[res+1] = height;	
				neighborIs[2].close();
			}

			// sw
			if (neighbors[5] != null) {
				neighborIs[5] = new DataInputStream(new FileInputStream(neighbors[5].src));
				neighborIs[5].read(buf);
				
				final short height = (short) ( ((buf[res*2-2] & 0xff) << 0x8) | (buf[res*2-1] & 0xff) );				
				data[(res+2)*(res+1)] = height;	
				neighborIs[5].close();
			}

			// se
			if (neighbors[7] != null) {
				neighborIs[7] = new DataInputStream(new FileInputStream(neighbors[7].src));
				neighborIs[7].read(buf);
				
				final short height = (short) ( ((buf[0] & 0xff) << 0x8) | (buf[1] & 0xff) );				
				data[(res+2)*(res+2)-1] = height;		
				neighborIs[7].close();
			}

			// n
			if (neighbors[1] != null) {
				neighborIs[1] = new DataInputStream(new FileInputStream(neighbors[1].src));
				neighborIs[1].skipBytes(2 * res * (res - 1));
				neighborIs[1].read(buf);
				
				for (int i=0; i < res; i++) {
					final short height = (short) ( ((buf[i*2] & 0xff) << 0x8) | (buf[i*2+1] & 0xff) );					
					data[i+1] = height;
				}					
				neighborIs[1].close();
			}

			// s
			if (neighbors[6] != null) {
				neighborIs[6] = new DataInputStream(new FileInputStream(neighbors[6].src));
				neighborIs[6].read(buf);
				
				for (int i=0; i < res; i++) {
					final short height = (short) ( ((buf[i*2] & 0xff) << 0x8) | (buf[i*2+1] & 0xff) );
					data[(res+2)*(res+1)+i+1] = height;
				}					
				neighborIs[6].close();
			}

			// w
			if (neighbors[3] != null) {
				neighborIs[3] = new DataInputStream(new FileInputStream(neighbors[3].src));
				for (int y=0; y<res; y++) { // row (y-axis)
					neighborIs[3].read(buf);
					
					final short height = (short) ( ((buf[res*2-2] & 0xff) << 0x8) | (buf[res*2-1] & 0xff) );					
					data[(res+2)*(y+1)] = height;						
				}				
				neighborIs[3].close();
			}

			// e
			if (neighbors[4] != null) {
				neighborIs[4] = new DataInputStream(new FileInputStream(neighbors[4].src));
				for (int y=0; y<res; y++) { // row (y-axis)
					neighborIs[4].read(buf);
					
					final short height = (short) ( ((buf[0] & 0xff) << 0x8) | (buf[1] & 0xff) );					
					data[(res+2)*(y+1) + (res+1)] = height;						
				}				
				neighborIs[4].close();
			}			

			//////////////
			
			final BufferedImage img = new BufferedImage(res+2, res+2, BufferedImage.TYPE_INT_ARGB);
			for (int y = 0; y < res; y++) {				
				
				Latitude centerLat = 
					new Latitude(
							srtm.northEast.latitude.degrees() 
							+ ( y * (srtm.southWest.latitude.degrees() - srtm.northEast.latitude.degrees()) 
								/ (res - 1)
							  ));
				
				for (int x = 0; x < res; x++) {				
					
					Longitude centerLon = 
						new Longitude(
								srtm.southWest.longitude.degrees()
								+ ( x * (srtm.northEast.longitude.degrees() - srtm.southWest.longitude.degrees())
									/ (res - 1)
								  ));
					
					final double slopeDegree = findMaxSlope(
								new short[]{ data[(x) + (y)*(res+2)],   data[(x+1) + (y)*(res+2)  ], data[(x+2) + (y)*(res+2)],
											 data[(x) + (y+1)*(res+2)], data[(x+1) + (y+1)*(res+2)], data[(x+2) + (y+1)*(res+2)],
											 data[(x) + (y+2)*(res+2)], data[(x+1) + (y+2)*(res+2)], data[(x+2) + (y+2)*(res+2)]}
								, new LatLong(centerLat, centerLon), srtm.arcSecond);
					img.setRGB(x, y,  cm.getEncodedColor( slopeDegree ));
				}				
			}
			
			return img;
			
		} catch (IOException e) {
			throw new RuntimeException(e);
		}finally {
			try { srtmis.close(); } catch (Exception ignored) {}
			for (DataInputStream nis : neighborIs) {
				try { nis.close(); } catch (Exception ignored) {}
			}
		}						
	}
	
	/**
	 * @param matrix an array with 9 height value.
	 *        [ nw, n, ne,
	 *          w,  c,  e,
	 *          sw, s, se ]
	 * @return slope degree. If unknown, a negative value will be returned.
	 */
	private static double findMaxSlope(short[] heights, LatLong center, int arcSecond) {
		if (heights.length != 9) {
			throw new IllegalArgumentException();
		}
		double slope = -1; // degree
		
		if (heights[4] == Short.MIN_VALUE) {
			return slope;
		}
		
		final double offset = (double)arcSecond / 60.0 / 60.0;
		final Measure apprXDistance  = center.distanceTo( 
										new LatLong(
											new Latitude(center.latitude.degrees()), 
											new Longitude(center.longitude.degrees() + offset)) );
		
		final Measure apprYDistance  = center.distanceTo( 
										new LatLong(
											new Latitude(center.latitude.degrees() + offset), 
											new Longitude(center.longitude.degrees())) );
		final Measure apprXYDistance  = center.distanceTo( 
										new LatLong(
											new Latitude(center.latitude.degrees() + offset), 
											new Longitude(center.longitude.degrees() + offset)) );
		
		// nw
		if (heights[0] != Short.MIN_VALUE) {
			double s = Math.toDegrees( Math.atan( Math.abs(heights[0] - heights[4]) / apprXYDistance.doubleValue() ) );
			if (s > slope) {
				slope = s;
			}
		}
		
		// n 
		if (heights[1] != Short.MIN_VALUE) {
			double s = Math.toDegrees( Math.atan( Math.abs(heights[1] - heights[4]) / apprYDistance.doubleValue() ) );
			if (s > slope) {
				slope = s;
			}
		}
		
		// ne
		if (heights[2] != Short.MIN_VALUE) {
			double s = Math.toDegrees( Math.atan( Math.abs(heights[2] - heights[4]) / apprXYDistance.doubleValue() ) );
			if (s > slope) {
				slope = s;
			}
		}
		
		// w
		if (heights[3] != Short.MIN_VALUE) {
			double s = Math.toDegrees( Math.atan( Math.abs(heights[3] - heights[4]) / apprXDistance.doubleValue() ) );
			if (s > slope) {
				slope = s;
			}
		}
		
		// e
		if (heights[5] != Short.MIN_VALUE) {
			double s = Math.toDegrees( Math.atan( Math.abs(heights[5] - heights[4]) / apprXDistance.doubleValue() ) );
			if (s > slope) {
				slope = s;
			}
		}
		
		// sw
		if (heights[6] != Short.MIN_VALUE) {
			double s = Math.toDegrees( Math.atan( Math.abs(heights[6] - heights[4]) / apprXYDistance.doubleValue() ) );
			if (s > slope) {
				slope = s;
			}
		}
		
		// s
		if (heights[7] != Short.MIN_VALUE) {
			double s = Math.toDegrees( Math.atan( Math.abs(heights[7] - heights[4]) / apprYDistance.doubleValue() ) );
			if (s > slope) {
				slope = s;
			}
		}
		
		// se
		if (heights[8] != Short.MIN_VALUE) {
			double s = Math.toDegrees( Math.atan( Math.abs(heights[8] - heights[4]) / apprXYDistance.doubleValue() ) );
			if (s > slope) {
				slope = s;
			}
		}
		
		
		return slope;
	}
}
